package com.tmt.im.manager.config;

import com.tmt.im.manager.config.beans.*;
import com.tmt.im.manager.service.IMUserService;
import com.tmt.im.manager.service.IgniteService;
import com.tmt.im.manager.service.UserDetailsServiceImpl;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.security.oauth2.client.OAuth2ClientProperties;
import org.springframework.boot.autoconfigure.security.oauth2.resource.OAuth2ResourceServerProperties;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.core.annotation.Order;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService;
import org.springframework.security.oauth2.server.resource.introspection.NimbusOpaqueTokenIntrospector;
import org.springframework.security.oauth2.server.resource.introspection.OpaqueTokenIntrospector;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.session.ConcurrentSessionControlAuthenticationStrategy;
import org.springframework.security.web.session.InvalidSessionStrategy;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.session.FindByIndexNameSessionRepository;
import org.springframework.session.Session;
import org.springframework.web.client.RestOperations;
import org.springframework.web.client.RestTemplate;

import java.text.MessageFormat;
import java.time.Duration;
import java.util.Map;

/**
 * @Description TODO
 * @Author wuyi
 * @Date 2024/7/17 16:53
 * @Version 1.0
 **/
@Configuration
@EnableMethodSecurity(jsr250Enabled = true)
@EnableWebSecurity
public class SecurityConfig<S extends Session> implements ApplicationContextAware {
    private ApplicationContext applicationContext;

    private final OAuth2ClientProperties oAuth2ClientProperties;

    private final OAuth2AuthorizedClientService authorizedClientService;

    private final FindByIndexNameSessionRepository<S> sessionRepository;

    private final RedisConnectionFactory redisConnectionFactory;

    private final String swaggerUiPath;


    private final String apiUrlPrefix;

    private final String apiDocsPath;

    private final RestTemplate restTemplate;

    private final IgniteService igniteService;

    private final IMUserService imUserService;


    public SecurityConfig(@Value("${api-url-prefix}")
                          String apiUrlPrefix,
                          @Value("${springdoc.swagger-ui.path}")
                          String swaggerUiPath,
                          @Value("${springdoc.api-docs.path}")
                          String apiDocsPath,
                          @Qualifier("myRestTemplate")
                          RestTemplate restTemplate,
                          OAuth2ClientProperties oAuth2ClientProperties,
                          OAuth2AuthorizedClientService authorizedClientService,
                          FindByIndexNameSessionRepository<S> sessionRepository,
                          RedisConnectionFactory redisConnectionFactory,
                          IgniteService igniteService,
                          IMUserService imUserService) {
        this.apiUrlPrefix = apiUrlPrefix;
        this.swaggerUiPath = swaggerUiPath;
        this.apiDocsPath = apiDocsPath;
        this.restTemplate = restTemplate;
        this.oAuth2ClientProperties = oAuth2ClientProperties;
        this.authorizedClientService = authorizedClientService;
        this.sessionRepository = sessionRepository;
        this.redisConnectionFactory = redisConnectionFactory;
        this.igniteService = igniteService;
        this.imUserService = imUserService;
    }

    @Primary
    @Bean
    public RedisTemplate<String, Object> redisTemplate() {
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(redisConnectionFactory);
        redisTemplate.setKeySerializer(RedisSerializer.string());
        redisTemplate.setHashKeySerializer(RedisSerializer.string());
        redisTemplate.afterPropertiesSet();
        return redisTemplate;
    }

    @Bean
    @Order(1)
    public SecurityFilterChain authorizationClientSecurityFilterChain(HttpSecurity http) throws Exception {
        http.authorizeHttpRequests((authorize) -> authorize
                        .requestMatchers(
                                new AntPathRequestMatcher(swaggerUiPath.split("\\.")[0] + "**"),
                                new AntPathRequestMatcher(swaggerUiPath.split("\\.")[0] + "/**"),
                                new AntPathRequestMatcher(apiDocsPath.split("\\.")[0] + "**"),
                                new AntPathRequestMatcher(apiDocsPath.split("\\.")[0] + "/**"),
                                new AntPathRequestMatcher(MessageFormat.format("{0}/health/**", apiUrlPrefix)),
                                new AntPathRequestMatcher(MessageFormat.format("{0}/param/version/**", apiUrlPrefix)),
                                new AntPathRequestMatcher(MessageFormat.format("{0}/yunxin/eventNotify", apiUrlPrefix)),
                                new AntPathRequestMatcher(MessageFormat.format("{0}/externalNotify/**", apiUrlPrefix)),
                                new AntPathRequestMatcher(MessageFormat.format("{0}/file/avatar/**", apiUrlPrefix)),
                                new AntPathRequestMatcher(MessageFormat.format("{0}/file/download/**", apiUrlPrefix)),
                                new AntPathRequestMatcher(MessageFormat.format("{0}/test/**", apiUrlPrefix))
                        ).permitAll()
                        .anyRequest().authenticated())
                .csrf(AbstractHttpConfigurer::disable)
                .cors(AbstractHttpConfigurer::disable)
                .logout(c -> c
                        .clearAuthentication(false)
                        .logoutSuccessHandler(new MyLogoutSuccessHandler(restTemplate, oAuth2ClientProperties, authorizedClientService)))
                //.oauth2Client(Customizer.withDefaults())
                .oauth2Login(c -> c.successHandler(new MyLoginSuccessHandler(igniteService, imUserService)))
                //.oauth2Login(withDefaults())
                //.formLogin(c -> c.successHandler(new MyLoginSuccessHandler(authorizedClientService)))
                //.userDetailsService(userDetailsService())
                .sessionManagement((configurer) -> configurer
                        .sessionConcurrency(c -> c.maximumSessions(1)
                                .maxSessionsPreventsLogin(false)
                                .sessionRegistry(sessionRegistry())
                                .expiredSessionStrategy(new MySessionInformationExpiredStrategy(oAuth2ClientProperties))
                        )
                        .sessionCreationPolicy(SessionCreationPolicy.STATELESS))
//                .securityContext(c->c.securityContextRepository())
                // Accept access tokens for User Info and/or Client Registration
                .oauth2ResourceServer(c -> c.jwt(r -> r.jwtAuthenticationConverter(new MyJwtAuthenticationConverter(igniteService, imUserService))))
        //.oauth2ResourceServer(c ->c.jwt(Customizer.withDefaults()))
        //.oauth2ResourceServer((oauth2) -> oauth2
        //        .opaqueToken(Customizer.withDefaults()))
        ;

        return http.build();
    }

    @Bean
    public OpaqueTokenIntrospector introspector(RestTemplateBuilder builder, OAuth2ResourceServerProperties properties) {
        RestOperations rest = builder
                .basicAuthentication(properties.getOpaquetoken().getClientId(), properties.getOpaquetoken().getClientSecret())
                .setConnectTimeout(Duration.ofSeconds(60))
                .setReadTimeout(Duration.ofSeconds(60))
                .build();

        return new NimbusOpaqueTokenIntrospector(properties.getOpaquetoken().getIntrospectionUri(), rest);
    }

//    @Bean
//    public JwtDecoder jwtDecoder() {
//        return NimbusJwtDecoder.withJwkSetUri(this.jwkSetUri).build();
//    }

    @Bean
    public MySpringSessionBackedSessionRegistry<S> sessionRegistry() {
        return new MySpringSessionBackedSessionRegistry<>(sessionRepository);
    }

    @Bean
    public InvalidSessionStrategy invalidSessionStrategy() {
        return new MyInvalidSessionStrategy();
    }

    @Bean
    public ConcurrentSessionControlAuthenticationStrategy SessionAuthenticationStrategy() {
        return new ConcurrentSessionControlAuthenticationStrategy(sessionRegistry());
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    /**
     * userDetailsService对象必须定义，不然无法访问不需要鉴权的接口。
     *
     * @return
     */
    @Bean
    public UserDetailsService userDetailsService() {
        Map<String, UserDetailsServiceImpl> map = applicationContext.getBeansOfType(UserDetailsServiceImpl.class);
        return map.values().stream().toList().get(0);
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }
}
